#include "Builder/NinjaPlan.hpp"

#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fstream>
#include <string>
#include <string_view>
#include <utility>

namespace cabin {

template <typename Range>
static std::string joinWithSpace(const Range& range) {
  if (range.empty()) {
    return "";
  }
  return fmt::format("{}", fmt::join(range, " "));
}

NinjaPlan::NinjaPlan(std::filesystem::path outBasePath)
    : outBasePath_(std::move(outBasePath)) {}

void NinjaPlan::reset() {
  edges_.clear();
  defaultTargets_.clear();
  testTargets_.clear();
}

void NinjaPlan::addEdge(NinjaEdge edge) { edges_.push_back(std::move(edge)); }

void NinjaPlan::addDefaultTarget(std::string target) {
  defaultTargets_.push_back(std::move(target));
}

void NinjaPlan::setTestTargets(std::vector<std::string> testTargets) {
  testTargets_ = std::move(testTargets);
}

void NinjaPlan::writeFiles(const NinjaToolchain& toolchain) const {
  writeBuildNinja();
  writeConfigNinja(toolchain);
  writeRulesNinja();
  writeTargetsNinja();
}

void NinjaPlan::writeBuildNinja() const {
  std::ofstream buildFile(outBasePath_ / "build.ninja");
  buildFile << "# Generated by Cabin\n";
  buildFile << "ninja_required_version = 1.11\n\n";
  buildFile << "include config.ninja\n";
  buildFile << "include rules.ninja\n";
  buildFile << "include targets.ninja\n\n";
  if (!defaultTargets_.empty()) {
    buildFile << "default " << joinWithSpace(defaultTargets_) << '\n';
  }
}

void NinjaPlan::writeConfigNinja(const NinjaToolchain& toolchain) const {
  std::ofstream cfg(outBasePath_ / "config.ninja");
  cfg << "# Build variables\n";
  cfg << "CXX = " << toolchain.cxx << '\n';
  cfg << "CXXFLAGS = " << toolchain.cxxFlags << '\n';
  cfg << "DEFINES = " << toolchain.defines << '\n';
  cfg << "INCLUDES = " << toolchain.includes << '\n';
  cfg << "LDFLAGS = " << toolchain.ldFlags << '\n';
  cfg << "LIBS = " << toolchain.libs << '\n';
  cfg << "AR = " << toolchain.archiver << '\n';
}

void NinjaPlan::writeRulesNinja() const {
  std::ofstream rules(outBasePath_ / "rules.ninja");

  rules << "rule cxx_compile\n";
  rules << "  command = $CXX $DEFINES $INCLUDES $CXXFLAGS $extra_flags -c $in "
           "-o $out\n";
  rules << "  description = Building CXX object $out\n\n";

  rules << "rule cxx_link_exe\n";
  rules << "  command = $CXX $in $LDFLAGS $LIBS -o $out\n";
  rules << "  description = Linking CXX executable $out\n\n";

  rules << "rule cxx_link_static_lib\n";
  rules << "  command = rm -f $out && $AR rcs $out $in\n";
  rules << "  description = Linking CXX static library $out\n\n";
}

void NinjaPlan::writeTargetsNinja() const {
  std::ofstream targetsFile(outBasePath_ / "targets.ninja");

  for (const NinjaEdge& edge : edges_) {
    targetsFile << "build " << joinWithSpace(edge.outputs);
    targetsFile << ": " << edge.rule;
    if (!edge.inputs.empty()) {
      targetsFile << ' ' << joinWithSpace(edge.inputs);
    }
    if (!edge.implicitInputs.empty()) {
      targetsFile << " | " << joinWithSpace(edge.implicitInputs);
    }
    if (!edge.orderOnlyInputs.empty()) {
      targetsFile << " || " << joinWithSpace(edge.orderOnlyInputs);
    }
    targetsFile << '\n';
    for (const auto& [key, value] : edge.bindings) {
      targetsFile << "  " << key << " = " << value << '\n';
    }
    targetsFile << '\n';
  }

  if (!defaultTargets_.empty()) {
    targetsFile << "build all: phony " << joinWithSpace(defaultTargets_) << '\n'
                << '\n';
  }
  if (!testTargets_.empty()) {
    targetsFile << "build tests: phony " << joinWithSpace(testTargets_) << '\n'
                << '\n';
  }
}

} // namespace cabin
